Skip to content

feat(confirmations): scan EIP-712 address fields via phishing controller - #45521

Open
wzrdk3lly wants to merge 2 commits into
MetaMask:mainfrom
wzrdk3lly:feat/core-signature-address-scan
Open

wzrdk3lly wants to merge 2 commits into
MetaMask:mainfrom
wzrdk3lly:feat/core-signature-address-scan

Conversation

@wzrdk3lly

@wzrdk3lly wzrdk3lly commented Aug 13, 2026 •

Copy link
Copy Markdown

Summary

Ticket: PSAFE-441

MetaMask validates eth_signTypedData_v3 and eth_signTypedData_v4 requests with PPOM, which inspects the address fields of a signature. PPOM's threat data is refreshed on a delay, so an address that has been flagged recently can still be reported as benign, and it does not cover every chain or protocol.

Separately, the real-time per-address scan (/address/evm/scan) is only applied to a few fields today: the token verifyingContract, permit spender, and delegation delegate. Addresses carried in any other field — recipients, makers, tokens, custom protocol fields — receive no real-time scan at all.

This change wires a schema-driven extractor (added to @metamask/phishing-controller in MetaMask/core#9875) that returns up to 10 distinct address-typed values found in a typed-data message, including nested structs and arrays, matched by declared type rather than field name. When PPOM has not already flagged the request, those addresses are passed to the real-time address scan. Results reuse the existing address cache, so fields already scanned elsewhere are not requested again. A confirmation alert surfaces any flagged address using the existing alert template, naming the flagged field and address. If some addresses could not be checked because the cap, depth limit, or work budget was reached, a separate caution is shown.

Changes

  • app/scripts/lib/trust-signals/scan-unvalidated-signature.ts (new) — extracts up to 10 address-typed fields and scans each against the real-time API after PPOM; excludes the signer, zero address, and permit spender when verifyingContract is already scanned
  • ui/pages/confirmations/hooks/alerts/useSignatureAddressAlerts.ts (new) — React hook that reads scan results and returns alerts for malicious/warning addresses (naming the field) plus a caution when the walk was incomplete
  • app/scripts/lib/ppom/ppom-middleware.ts — calls scanUnvalidatedSignatureAddresses after PPOM resolves for signing methods when PPOM has not flagged the request
  • app/scripts/lib/ppom/ppom-util.ts — updates validateRequestWithPPOM return type to Promise<SecurityAlertResponse> in all paths
  • ui/pages/confirmations/hooks/useConfirmationAlerts.ts — wires useSignatureAddressAlerts into the confirmation alert pipeline
  • Locales — adds alertMessageSignatureAddressMalicious, alertMessageSignatureAddressWarning, alertMessageSignatureAddressScanIncomplete, alertReasonSignatureAddressScanIncomplete

Dependencies

Blocked on MetaMask/core#9875. The package.json dependency on @metamask/phishing-controller must be bumped to the version that exports extractSignatureAddresses before this can merge.

References

Test plan

  • Open a v3/v4 typed-data signature on a supported chain with a known-malicious address in a non-from field (e.g. recipient, token, maker) — confirm a Danger alert appears naming the field and shortened address
  • Open a permit-type signature — confirm spender alert comes from useSpenderAlerts, not duplicated here
  • Trigger a message with more than 10 distinct address fields — confirm the scan-incomplete caution appears
  • Open a signature on an unsupported chain — confirm no scan fires
  • Confirm no alert regression on standard transaction confirmations

Screenshots/Recordings

Signing the same typed-data request on a build without this change and a build with it. The request carries a flagged address in a non-permit field.

Before (no scan of arbitrary address fields — the flagged address is not surfaced):

Address.Scanning.Before.mov

After (the flagged address is scanned and the standard warning is shown):

Address.Scanning.After.mov

Wires the signature address scanning feature into the confirmation flow
using `extractSignatureAddresses` from `@metamask/phishing-controller`
rather than a local copy of the extractor.

After PPOM validates a v3/v4 typed-data signature request and returns
Benign or Errored, address-typed fields in the EIP-712 message are
extracted and scanned against the security-alerts API. Results are
surfaced as alerts via the new `useSignatureAddressAlerts` hook, naming
the specific field (recipient, token, maker, etc.) in each alert.

Note: requires a bump to the `@metamask/phishing-controller` version
that exports `extractSignatureAddresses` (pending MetaMask/core PR).

Related: MetaMask#45058
@wzrdk3lly
wzrdk3lly requested a review from a team as a code owner August 13, 2026 23:50
@github-actions

Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit e9638eb. Configure here.

@matthewwalsh0
matthewwalsh0 requested a review from jpuri August 14, 2026 11:01
).catch((error) => {
console.error('Error scanning signature address', error);
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ensure that app state is also cleared, else can this alert show for next valid signature also ?

@wzrdk3lly wzrdk3lly Aug 14, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the app state is not explicitly cleared between requests. Cached results expire after 15
minutes via TTL. The alert won't show on the next signature unless that request also contains the
same flagged address, since the hook only reads cache entries for addresses present in the current
confirmation's typed data.

@wzrdk3lly
wzrdk3lly requested a review from jpuri August 14, 2026 17:02
pull Bot pushed a commit to Reality2byte/core that referenced this pull request Sep 10, 2026
…12 typed-data scanning (MetaMask#10170)

## Explanation
Ticket:
[PSAFE-441](https://consensyssoftware.atlassian.net/browse/PSAFE-441)

Supersedes [MetaMask#9875](MetaMask#9875) (same
change, opened from a core branch so changelog CI can resolve the head).
Review comments on that PR were addressed there.

MetaMask validates `eth_signTypedData_v3` and `eth_signTypedData_v4`
requests with PPOM, which inspects the address fields of a signature.
PPOM's threat data is refreshed on a delay, so an address that has been
flagged recently can still be reported as benign, and it does not cover
every chain or protocol.

Separately, the real-time per-address scan (`/address/evm/scan`) is only
applied to a few fields today: the token `verifyingContract`, permit
`spender`, and delegation `delegate`. Addresses carried in any other
field — recipients, makers, tokens, custom protocol fields — receive no
real-time scan at all.

This change adds a schema-driven extractor that returns up to 10
distinct `address`-typed values found in a typed-data message (including
nested structs and arrays, matched by declared type rather than field
name), and reports when the message could not be fully walked because
the address cap, depth limit, or work budget was reached. When PPOM has
not already flagged the request, the extracted addresses are passed to
the real-time address scan. Results reuse the existing address cache, so
fields already scanned elsewhere are not requested again.

### What this exports

- `extractSignatureAddresses(typedData, options)` — walks
`types`/`primaryType`/`message`, returns `{ addresses, fields, overflow,
maxAddresses }`
- `ExtractedSignatureAddresses` — return type
- `ExtractSignatureAddressesOptions` — options: `exclude` (addresses to
skip, e.g. the signer), `excludeFields` (top-level field names to skip,
e.g. `spender` when already covered by an existing scan), `maxAddresses`
(optional cap override; default 10, hard ceiling 50)
- `DEFAULT_MAX_SIGNATURE_ADDRESSES` and
`MAX_SIGNATURE_ADDRESSES_CEILING` — exported so clients can word
overflow copy and stay aligned with the default/ceiling

### Safeguards

- Cap of 10 distinct addresses per message by default, overridable via
`maxAddresses` up to 50
- Max traversal depth of 12
- Max 5000 nodes visited
- `overflow: true` is returned whenever the walk is incomplete so
callers can surface a caution

## References

- Prior fork PR (same change): MetaMask#9875
- MetaMask Extension (client wiring): MetaMask/metamask-extension#45521
- MetaMask Mobile (client wiring): MetaMask/metamask-mobile#34786
- Prior implementation with local extractor copy (Extension):
MetaMask/metamask-extension#45058
- Prior implementation with local extractor copy (Mobile):
MetaMask/metamask-mobile#34428

## Changelog

See `packages/phishing-controller/CHANGELOG.md`.

## Checklist

- [x] Tests written and passing
- [x] Changelog updated
- [x] Exports added to `index.ts`
- [ ] Version bump (pending release cut)

[PSAFE-441]:
https://consensyssoftware.atlassian.net/browse/PSAFE-441?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Security-adjacent signing flow support: incorrect normalization or
missed addresses could weaken typed-data scanning when clients adopt the
export, though behavior is heavily tested and bounded with overflow
signaling.
> 
> **Overview**
> Adds **`extractSignatureAddresses`**, a schema-driven helper that
walks EIP-712 `types`/`primaryType`/`message` and collects distinct
**`address`-typed** values (including nested structs and arrays) for
downstream real-time address scanning—not hard-coded field names like
`spender` or `to`.
> 
> The function **normalizes** values the same way signing does
(hex/decimal, leading 20 bytes, lowercase dedupe), supports
**`exclude`**, top-level **`excludeFields`**, and a configurable
distinct-address cap (**default 10**, ceiling **50**). It returns
**`addresses`**, per-address **`fields`** for alert copy, and
**`overflow`** when traversal hits caps, depth (12), or a 5000-node
budget so clients can warn when not every address was collected.
**`DEFAULT_MAX_SIGNATURE_ADDRESSES`** and
**`MAX_SIGNATURE_ADDRESSES_CEILING`** are exported from **`index.ts`**
alongside the new types.
> 
> Ships **`signature-address-extraction.ts`**, a large Jest suite
(including **`@metamask/eth-sig-util`** parity checks), changelog entry,
and **`eth-sig-util`** as a **devDependency** only—no
**`PhishingController`** wiring in this PR.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
732e1c3. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Alex Donesky <adonesky@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants